/***************************************************************
*  Copyright (c) 2007 by GroupMe! Team (www.groupme.net)
*  All rights reserved.
*
*  This file is part of the GroupMe! Project. Source code of 
*  this project is closed and redistribution of this code is
*  prohibited. 
*  
*  Contact: http://www.groupme.net
*
*  This copyright notice MUST APPEAR in all copies of the file!
***************************************************************/
package net.groupme.search.strategy.folkrank;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import cern.colt.matrix.DoubleMatrix1D;
import cern.colt.matrix.DoubleMatrix2D;
import cern.colt.matrix.impl.DenseDoubleMatrix1D;
import cern.colt.matrix.impl.SparseDoubleMatrix2D;

import net.groupme.exceptions.UserNotExistingException;
import net.groupme.model.ObjectFactory;
import net.groupme.search.strategy.RankingStrategy;
import net.groupme.search.strategy.model.PreferencesInTags;
import net.groupme.search.strategy.model.RankingPreferences;
import net.groupme.search.strategy.model.TAS;
import net.groupme.search.strategy.model.TASImpl;
import net.groupme.search.strategy.model.WeightedIdentifiableComparatorByWeight;
import net.groupme.search.strategy.model.WeightedResource;
import net.groupme.search.strategy.model.WeightedTag;
import net.groupme.search.strategy.model.WeightedUser;

/**
 * created on 10 Jan 2008
 * 
 * This class implements the traditional FolkRank ranking strategy. 
 * 
 * @author Fabian Abel, <a href="mailto:abel@l3s.de">abel@l3s.de</a>
 * @author last edited by: $Author: fabian $
 * 
 * @version $Revision: 1.7 $ $Date: 2010-02-03 10:55:39 $
 */
public class NormalFolkRankRankingStrategy implements RankingStrategy{
	
	/** Logger for this class and subclasses */
	protected final Log logger = LogFactory.getLog(this.getClass());
	
	/** the Adjacency Matrix a normal FolkRank would construct */
	protected DoubleMatrix2D adjacencyMatrix = null;
	
	//Mappings:
	protected Map<Integer, Integer> userIdColumMapping = null;
	protected Map<Integer, Integer> tagIdColumMapping = null;
	protected Map<Integer, Integer> resourceIdColumMapping = null;
	

	/**
	 * Creates a new NormalFolkRankRankingStrategy instance. 
	 */
	public NormalFolkRankRankingStrategy() {
		super();
		userIdColumMapping = new HashMap<Integer, Integer>();
		tagIdColumMapping = new HashMap<Integer, Integer>();
		resourceIdColumMapping = new HashMap<Integer, Integer>();
	}

	/* (non-Javadoc)
	 * @see net.groupme.search.strategy.RankingStrategy#getDescription()
	 */
	public String getDescription() {
		return "A normal FolkRank computation..";
	}

	/* (non-Javadoc)
	 * @see net.groupme.search.strategy.RankingStrategy#getName()
	 */
	public String getName() {
		return "FolkRank";
	}

	/* (non-Javadoc)
	 * @see net.groupme.search.strategy.RankingStrategy#rankResources(net.groupme.search.strategy.model.RankingPreferences)
	 */
	public List<WeightedResource> rankResources(RankingPreferences preferences) {
		//compute FolkRank Vector:
		DoubleMatrix1D folkrankVector = computeFolkrankVector((PreferencesInTags)preferences);
		
		//output the result:
		Statement sqlStatement = null;
		ResultSet result = null;
		List<WeightedResource> ranking = new ArrayList<WeightedResource>();
		try {
			sqlStatement = ObjectFactory.ds.getConnection().createStatement();
			result = sqlStatement.executeQuery("SELECT id, name, description, url, type FROM Resource");
			while(result.next()){	
				if(resourceIdColumMapping.containsKey(result.getInt(1))){
					ranking.add(
							new WeightedResource(
									ObjectFactory.getInMemoryResource(result.getString(1), result.getString(2), result.getString(3), 
											result.getURL(4), result.getString(5)), 
							folkrankVector.get(resourceIdColumMapping.get(result.getInt(1)))));
	
				}
			}
			ObjectFactory.ds.getConnection().close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		Collections.sort(ranking, new WeightedIdentifiableComparatorByWeight());
		return ranking;
	}

	/* (non-Javadoc)
	 * @see net.groupme.search.strategy.RankingStrategy#rankTags(net.groupme.search.strategy.model.RankingPreferences)
	 */
	public List<WeightedTag> rankTags(RankingPreferences preferences) {
//		compute FolkRank Vector:
		DoubleMatrix1D folkrankVector = computeFolkrankVector((PreferencesInTags)preferences);
		
		//output the result:
		Statement sqlStatement = null;
		ResultSet result = null;
		List<WeightedTag> ranking = new ArrayList<WeightedTag>();
		try {
			sqlStatement = ObjectFactory.ds.getConnection().createStatement();
			result = sqlStatement.executeQuery("SELECT id, keyword FROM Tag where id in (Select distinct tagId from userTagGroupResource)");
			while(result.next()){	
				if(tagIdColumMapping.containsKey(result.getInt(1))){
					ranking.add(
							new WeightedTag(
									ObjectFactory.getInMemoryTag(result.getString(1), result.getString(2), ""), 
							folkrankVector.get(tagIdColumMapping.get(result.getInt(1)))));
	
				}
			}
			ObjectFactory.ds.getConnection().close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		Collections.sort(ranking, new WeightedIdentifiableComparatorByWeight());
		return ranking;
	}

	/* (non-Javadoc)
	 * @see net.groupme.search.strategy.RankingStrategy#rankUsers(net.groupme.search.strategy.model.RankingPreferences)
	 */
	public List<WeightedUser> rankUsers(RankingPreferences preferences) {
//		compute FolkRank Vector:
		DoubleMatrix1D folkrankVector = computeFolkrankVector((PreferencesInTags)preferences);
		
		//output the result:
		Statement sqlStatement = null;
		ResultSet result = null;
		List<WeightedUser> ranking = new ArrayList<WeightedUser>();
		try {
			sqlStatement = ObjectFactory.ds.getConnection().createStatement();
			result = sqlStatement.executeQuery("SELECT id FROM User where id in (Select distinct userId from userTagGroupResource)");
			while(result.next()){	
				if(userIdColumMapping.containsKey(result.getInt(1))){
					try {
						ranking.add(
								new WeightedUser(
										ObjectFactory.getUser(result.getInt(1)), 
								folkrankVector.get(userIdColumMapping.get(result.getInt(1)))));
					} catch (UserNotExistingException e) {
						e.printStackTrace();
						//do nothing - just ignore it...
					}
	
				}
			}
			ObjectFactory.ds.getConnection().close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		Collections.sort(ranking, new WeightedIdentifiableComparatorByWeight());
		return ranking;
	}
	
	/**
	 * Constructs the preference vector based on the given preferences and
	 * computes the FolkRank vector.
	 * @param prefs the preferences (interests in tags)
	 * @return the computed FolkRank vector
	 */
	protected DoubleMatrix1D computeFolkrankVector(PreferencesInTags prefs){
		if(adjacencyMatrix == null){
			constructAdjacencyMatrix();
		}
		DoubleMatrix1D preferenceVector = null;
		DoubleMatrix1D folkrankVector = null;
			
		//1. select personalization vector:
		preferenceVector = new DenseDoubleMatrix1D(adjacencyMatrix.rows());
		try {
			FolkRank.setPreferenceVektorForTags(preferenceVector, prefs, tagIdColumMapping);
		} catch (SQLException e) {
			e.printStackTrace();
		}
			
		//2. compute FolkRank:
		folkrankVector = FolkRank.computeFolkrank(adjacencyMatrix, preferenceVector);
		return folkrankVector;
	}

	
	/**
	 * constructs the Adjacency Matrix as a normal FolkRank would do. 
	 *
	 */
	protected void constructAdjacencyMatrix(){
		/* 1. Gather data: */
		Statement sqlStatement = null;
//		ResultSet result = null;
		List<TAS> tagAssignments = null;
		
		try {
			sqlStatement = ObjectFactory.ds.getConnection().createStatement();
		
			tagAssignments = addNormalMappings(sqlStatement);
			
			ObjectFactory.ds.getConnection().close();
		
		
			/* 2. Compute the Matrix: */
			
			//construct Matrix
			int numberOfVertices = userIdColumMapping.size() + tagIdColumMapping.size() + resourceIdColumMapping.size();
			adjacencyMatrix = new SparseDoubleMatrix2D(numberOfVertices,numberOfVertices);
			
			//fill adjacency matrix
			fillMatrixWithNormalTagAssignments(tagAssignments);
			
			//row-wise 1-Norm:
			computeRowWiseThe1Norm();
			
		}catch (SQLException e) {
			e.printStackTrace();
		}
		
	}
	
	/** the actual position within the Adjacency Matrix */
	protected int position = 0;
	
	/**
	 * Deduces the mappings between ids of entities (e.g. resources) and their position within the Adjacency Matrix
	 * and queries for all tag assignments.
	 * @param sqlStatement the SQL statement which is used to execute the query
	 * @return all tag assignments within the database
	 */
	protected List<TAS> addNormalMappings(Statement sqlStatement){
		position = 0;
		ResultSet result = null;
		try {		
			//get TAS and define mappings: 
			result = sqlStatement.executeQuery("Select userId, tagId, resourceId, groupId from userTagGroupResource  ");
			List<TAS> tagAssignments = new ArrayList<TAS>();
			TAS actualTas = null;
			while(result.next()){
				actualTas = new TASImpl(result.getInt(1), result.getInt(2),result.getInt(3),result.getInt(4));
				tagAssignments.add(actualTas);	
				if(!userIdColumMapping.containsKey(actualTas.getUserId())){;
					userIdColumMapping.put(actualTas.getUserId(), position);
					position++;
				}
				if(!tagIdColumMapping.containsKey(actualTas.getTagId())){
					tagIdColumMapping.put(actualTas.getTagId(), position);
					position++;
				}
				if(!resourceIdColumMapping.containsKey(actualTas.getResourceId())){
					resourceIdColumMapping.put(actualTas.getResourceId(), position);
					position++;
				}
			}
			//users that have not tagged but have build a group which contains at least one resource: TODO: Problem, wenn genau ein User betrachtet werden soll
			result = sqlStatement.executeQuery("SELECT distinct userId FROM userGroup where userId NOT in (Select distinct userId from userTagGroupResource) AND role = 'owner' ");
			while(result.next()){
				if(!userIdColumMapping.containsKey(result.getInt(1))){
					userIdColumMapping.put(result.getInt(1), position);
					position++;
				}
			}
			//untagged Resources: 
			result = sqlStatement.executeQuery("SELECT id FROM Resource where id NOT in (Select distinct resourceId from userTagGroupResource)");
			while(result.next()){
				if(!resourceIdColumMapping.containsKey(result.getInt(1))){
					resourceIdColumMapping.put(result.getInt(1), position);
					position++;
				}
			}
			return tagAssignments;
		}catch (SQLException e) {
			e.printStackTrace();
			return null;
		}
			
	}
	
	/**
	 * Fills the underlying Adjacency Matrix of this class according to the given tag assignments.
	 * @param tagAssignments normal tag assignments which are used to fill the Adjacency Matrix
	 */
	protected void fillMatrixWithNormalTagAssignments(List<TAS> tagAssignments){
		double userTagScore = 0.0;
		double tagResourceScore = 0.0;
		double userResourceScore = 0.0;
		int userPos = 0, tagPos = 0, resourcePos = 0;
		for (TAS tas : tagAssignments) {
			try{
				userTagScore = FolkRank.computeUserTagWeight(tagAssignments, tas.getUserId(), tas.getTagId());
				tagResourceScore = FolkRank.computeTagResourceWeight(tagAssignments, tas.getTagId(), tas.getResourceId());
				userResourceScore = FolkRank.computeUserResourceWeight(tagAssignments, tas.getUserId(), tas.getResourceId());
			}catch (SQLException e) {
				e.printStackTrace();
			}
			
			//get indices
			userPos = userIdColumMapping.get(tas.getUserId());
			tagPos = tagIdColumMapping.get(tas.getTagId());
			resourcePos = resourceIdColumMapping.get(tas.getResourceId());
			
			//set userTag-Score
			adjacencyMatrix.set(userPos, tagPos, userTagScore);
			adjacencyMatrix.set(tagPos,userPos, userTagScore);
			
			//set tagResource-Score:
			adjacencyMatrix.set(tagPos, resourcePos, tagResourceScore);
			adjacencyMatrix.set(resourcePos, tagPos, tagResourceScore);
			
			//set userResource-Score:
			adjacencyMatrix.set(userPos, resourcePos, userResourceScore);
			adjacencyMatrix.set(resourcePos, userPos, userResourceScore);
		}
	}
	
	/**
	 * Norms the Adjacency Matrix (row-wise) with the 1-Norm.
	 */
	protected void computeRowWiseThe1Norm(){
		double rowSum = 0.0;
		Double normTmp = 0.0;
		for(int row=0; row<adjacencyMatrix.rows(); row++){
			rowSum = 0.0;
			for(int column=0; column<adjacencyMatrix.columns(); column++){
				rowSum += adjacencyMatrix.get(row, column);
			}
			for(int column=0; column<adjacencyMatrix.columns(); column++){
				normTmp = adjacencyMatrix.get(row, column)/rowSum;
				adjacencyMatrix.set(row, column, (Double.isNaN(normTmp) ? 0.0 : normTmp));
			}
		}
	}
}
